package lol.clann.minecraft.springboot.plugin.bukkit.impl;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import lol.clann.minecraft.springboot.api.annotation.compatibility.CompatibilityChecked;
import lol.clann.minecraft.springboot.api.bukkit.constant.FluidCollisionOptionEnum;
import lol.clann.minecraft.springboot.api.bukkit.model.BoundingBox;
import lol.clann.minecraft.springboot.api.bukkit.model.RayTraceResult;
import lol.clann.minecraft.springboot.api.bukkit.utils.*;
import lol.clann.minecraft.springboot.api.model.reflect.FieldInvoker;
import lol.clann.minecraft.springboot.bukkit.utils.BukkitUtils;
import lol.clann.minecraft.springboot.bukkit.utils.NMSClasses;
import lol.clann.minecraft.springboot.bukkit.utils.NMSReflectUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.minecraft.server.v1_13_R2.DimensionManager;
import net.minecraft.server.v1_13_R2.WorldProvider;
import net.minecraft.server.v1_7_R4.WorldServer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.craftbukkit.v1_7_R4.CraftWorld;
import org.bukkit.entity.Entity;
import org.bukkit.event.Listener;
import org.bukkit.util.Vector;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

/**
 *
 * @author pyz
 * @date 2019/5/2 5:55 PM
 */
@Slf4j
@Service
@CompatibilityChecked
public class WorldUtilsImpl_all implements WorldUtils, Listener {
    private FieldInvoker worldTickTimes;
    @Getter
    private boolean useBlockPosition = false;
    @Autowired
    private LocationUtils locationUtils;
    @Autowired
    private ServerUtils serverUtils;
    @Autowired
    private VectorUtils vectorUtils;
    @Autowired
    private EntityUtils entityUtils;
    @Autowired
    private BoundingBoxUtils boundingBoxUtils;
    @Autowired
    private RayTraceUtils rayTraceUtils;
    private final BiMap<World, Object> nmsWorldMap = HashBiMap.create();

    @PostConstruct
    private void init() {
        try {
//            paper不支持这个
            worldTickTimes = new FieldInvoker(NMSReflectUtils.getField(NMSClasses.getMinecraftServer(), "worldTickTimes"));
        } catch (RuntimeException e) {
            LOGGER.warn("在类 {} 中找不到字段 {},无法获取世界tps. 此接口总是返回20:", NMSClasses.getMinecraftServer().getSimpleName(), "worldTickTimes", "lol.clann.minecraft.springboot.api.bukkit.utils.WorldUtils.getTps");
        }
    }

    @PreDestroy
    private void destroy() {
        nmsWorldMap.clear();
    }

    @Override
    public <T> T toNMS(World world) {
        return (T) ((CraftWorld) world).getHandle();
    }

    @Override
    public <T> World toBukkit(T nmsWorld) {
        if (nmsWorld == null) {
            return null;
        }
        World world = nmsWorldMap.inverse().get(nmsWorld);
        if (world != null) {
            return world;
        }
        for (World world1 : Bukkit.getWorlds()) {
            if (toNMS(world1) == nmsWorld) {
                nmsWorldMap.put(world1, nmsWorld);
                return world1;
            }
        }
        return null;
    }

    @Override
    public <T> T getTileEntity(Location location) {
        return getTileEntity(location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
    }

    @Override
    public <T> T getTileEntity(Block block) {
        return getTileEntity(block.getLocation());
    }

    @Override
    public <T> T getTileEntity(World world, int x, int y, int z) {
        if (BukkitUtils.getBukkitVersionNumber() < 11300) {
            return (T) ((CraftWorld) world).getTileEntityAt(x, y, z);
        } else {
            net.minecraft.server.v1_13_R2.World nmsWorld = toNMS(world);
            return (T) nmsWorld.getTileEntity(locationUtils.newBlockPosition(x, y, z));
        }
    }

    @Override
    public int getDimension(World world) {
        Objects.requireNonNull(world);
        return getDimenssionIdFromWorld(world);
    }

    @Override
    public World getByDimension(int demension) {
        for (World world : Bukkit.getWorlds()) {
            if (getDimension(world) == demension) {
                return world;
            }
        }
        return null;
    }

    @Override
    public double getTps(World world) {
        if (worldTickTimes == null) {
            return 20.0;
        }
        Hashtable<Integer, long[]> wtt = (Hashtable<Integer, long[]>) this.worldTickTimes.get(serverUtils.getNMSServer());
        double worldTickTime = mean(wtt.get(getDimension(world))) * 1e-6D;
        double worldTPS = Math.min(1000.0D / worldTickTime, 20.0D);
        return worldTPS;
    }

    /**
     *
     * @param start
     * @param direction
     * @param maxDistance
     * @param filter
     * @return
     */
    @Nullable
    @Override
    public Entity rayTraceEntity(Location start, Vector direction, double maxDistance, Predicate<Entity> filter) {
        World world = start.getWorld();
        Vector from = start.toVector();
        Vector to = start.toVector().add(direction.normalize().multiply(maxDistance));
        BoundingBox box = new BoundingBox(from.getX(), from.getY(), from.getZ(), to.getX(), to.getY(), to.getZ());
        List<Entity> entities = this.findEntitiesInBoundingBox(world, box, filter);
        double d0 = Double.MAX_VALUE;
        Entity result = null;
        for (Entity be : entities) {
            BoundingBox boundingBox = boundingBoxUtils.fromNMS(entityUtils.getAxisAlignedBB(be));
            RayTraceResult raytraceresult = boundingBox.rayTrace(from, to, Double.MAX_VALUE);
            if (raytraceresult == null) {
                continue;
            }
            double d1 = vectorUtils.squareDistanceTo(from, raytraceresult.getHitPosition());
            if (d1 < d0) {
                result = be;
                d0 = d1;
            }
        }
        return result;
    }

    /**
     *
     * @param start
     * @param direction
     * @param maxDistance
     * @param filter
     * @return
     */
    @Override
    public List<Entity> rayTraceEntities(Location start, Vector direction, double maxDistance, Predicate<Entity> filter) {
        World world = start.getWorld();
        Vector from = start.toVector();
        Vector to = start.toVector().add(direction.normalize().multiply(maxDistance));
        BoundingBox box = new BoundingBox(from.getX(), from.getY(), from.getZ(), to.getX(), to.getY(), to.getZ());
        List<Entity> entities = this.findEntitiesInBoundingBox(world, box, filter);
        List<Entity> rs = new ArrayList<>();
        for (Entity be : entities) {
            BoundingBox boundingBox = boundingBoxUtils.fromNMS(entityUtils.getAxisAlignedBB(be));
            RayTraceResult raytraceresult = boundingBox.rayTrace(from, to, Double.MAX_VALUE);
            if (raytraceresult == null) {
                continue;
            }
            rs.add(be);
        }
        return rs;
    }

    /**
     *
     * @param start
     * @param direction
     * @param maxDistance
     * @param fluidCollisionOption
     * @param ignorePassableBlocks 忽略不可碰撞的方块
     * @return
     */
    @Override
    public RayTraceResult rayTraceBlocks(Location start, Vector direction, double maxDistance, FluidCollisionOptionEnum fluidCollisionOption, boolean ignorePassableBlocks) {
        return rayTraceUtils.rayTraceBlocks(start, direction, maxDistance, fluidCollisionOption, ignorePassableBlocks);
    }

    /**
     * 获取立方体内的实体
     *
     * @param world
     * @param box
     * @param filter
     * @return
     */
    @Override
    public List<Entity> findEntitiesInBoundingBox(World world, BoundingBox box, Predicate<Entity> filter) {
        List<Entity> resulst = new ArrayList<>();
        net.minecraft.server.v1_13_R2.World nmsWorld = toNMS(world);
        List list = nmsWorld.getEntities(null, boundingBoxUtils.toNMS(box));
        for (Object e : list) {
            Entity be = entityUtils.toBukkit(e);
            if (be == null || (filter != null && !filter.test(be))) {
                continue;
            }
            resulst.add(be);
        }
        return resulst;
    }

    private int getDimenssionIdFromWorld(World world) {
        int dimenssionId;
        if (BukkitUtils.getBukkitVersionNumber() < 11300) {
            WorldServer nmsWorld = toNMS(world);
            dimenssionId = nmsWorld.dimension;
        } else {
            net.minecraft.server.v1_13_R2.WorldServer nmsWorld = toNMS(world);
            WorldProvider wp = nmsWorld.worldProvider;
            DimensionManager dm = wp.getDimensionManager();
            dimenssionId = dm.getDimensionID();
        }
        return dimenssionId;
    }

    private long mean(long[] values) {
        long sum = 0L;
        long[] var3 = values;
        int var4 = values.length;

        for (int var5 = 0; var5 < var4; ++var5) {
            long v = var3[var5];
            sum += v;
        }

        return sum / (long) values.length;
    }
}
